<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="coinfaces">

        <title>优雅地结束docker容器的init bash脚本</title>

    <link rel="stylesheet" href="../../theme/css/style.css">
    <link rel="stylesheet" href="../../theme/css/pygments.css">



</head>

<body>
    <div class="content">
<h1>优雅地结束docker容器的init bash脚本</h1>    <p>
        under
            <a href="../../tags/bash/">bash</a>
            <a href="../../tags/docker/">docker</a>
    </p>
    <p>
        in <a href="../../categories/tech/">tech</a>
    </p>
    <p>Published: 2016-08-14</p>


    <p>以下是一个docker容器的init脚本，它先启动若干服务，然后在前台开启一个永不退出的监视进程。在收到<tt class="docutils literal">docker stop</tt>发送的<tt class="docutils literal">SIGTERM</tt>信号后，结束脚本中运行的服务，并退出脚本。</p>
<div class="highlight"><pre><span></span><span class="ch">#!/bin/bash</span>

<span class="nb">trap</span> <span class="s1">&#39;stop_services; exit&#39;</span> SIGTERM SIGINT

<span class="nv">LOG_FILE</span><span class="o">=</span>/tmp/services.log
&gt;<span class="nv">$LOG_FILE</span>

service<span class="o">()</span> <span class="o">{</span>
    <span class="nb">local</span> <span class="nv">name</span><span class="o">=</span><span class="s2">&quot;</span><span class="nv">$1</span><span class="s2">&quot;</span>
    <span class="nb">local</span> <span class="nv">count</span><span class="o">=</span><span class="m">0</span>

    <span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
        <span class="nv">count</span><span class="o">=</span><span class="k">$((</span>count+1<span class="k">))</span>
        <span class="nb">echo</span> <span class="s2">&quot;</span><span class="nv">$name</span><span class="s2">: </span><span class="nv">$count</span><span class="s2">&quot;</span> &gt;&gt;<span class="nv">$LOG_FILE</span>
        sleep <span class="m">1</span>
    <span class="k">done</span>
<span class="o">}</span>

start_services<span class="o">()</span> <span class="o">{</span>
    <span class="nb">echo</span> <span class="s2">&quot;Start service 1&quot;</span>
    service s1 <span class="p">&amp;</span>
    <span class="nv">S1_PID</span><span class="o">=</span><span class="nv">$!</span>
    <span class="nb">echo</span> <span class="s2">&quot;Start service 2&quot;</span>
    service s2 <span class="p">&amp;</span>
    <span class="nv">S2_PID</span><span class="o">=</span><span class="nv">$!</span>
<span class="o">}</span>

stop_services<span class="o">()</span> <span class="o">{</span>
    <span class="nb">echo</span> <span class="s2">&quot;Stop service 2&quot;</span>
    <span class="nb">kill</span> <span class="nv">$S2_PID</span>
    <span class="nb">echo</span> <span class="s2">&quot;Stop service 1&quot;</span>
    <span class="nb">kill</span> <span class="nv">$S1_PID</span>
<span class="o">}</span>

<span class="nb">echo</span> <span class="s2">&quot;Script (PID=</span><span class="nv">$$</span><span class="s2">) running&quot;</span>

start_services

<span class="c1"># Make log available to &quot;docker logs&quot;</span>
tail -f <span class="nv">$LOG_FILE</span>
</pre></div>
<p>但在前台进程<tt class="docutils literal">tail</tt>退出前这个脚本永远不会执行<tt class="docutils literal">trap</tt>里设置的信号处理命令，因此它会一直运行，直到超时（默认）后被<tt class="docutils literal">docker stop</tt>用<tt class="docutils literal">SIGKILL</tt>强行结束，从下面的输出中可以看出<tt class="docutils literal">stop_services</tt>没有被执行，并且进程是被<tt class="docutils literal">SIGKILL</tt>结束的（返回了137）：</p>
<pre class="literal-block">
$ docker run -d --name test -v /tmp:/tmp debian /bin/bash /tmp/test_docker.sh; docker stop test
634d126a3d060a73a2b18689ae6f0c7cc340e276b7aeb031c9ff3fab14894e25
test
$ docker logs test; docker inspect -f '{{.State.ExitCode}}' test; docker rm test
Script (PID=1) running
Start service 1
Start service 2
s2: 1
s1: 1
s1: 2
...
s1: 10
s2: 11
s1: 11
137
test
</pre>
<p>在进一步解决这个问题之前，先用以下试验脚本了解一下bash脚本如何处理信号：</p>
<div class="highlight"><pre><span></span><span class="ch">#!/bin/bash</span>

<span class="nv">NAME</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">NAME</span><span class="k">:-</span><span class="nv">main</span><span class="si">}</span><span class="s2"> </span><span class="nv">$$</span><span class="s2">&quot;</span>
<span class="nv">TIME_SECS</span><span class="o">=</span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">TIME_SECS</span><span class="k">:-</span><span class="nv">2</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="nv">TICKS</span><span class="o">=</span><span class="k">$((</span>TIME_SECS*10<span class="k">))</span>

<span class="k">for</span> arg in <span class="s2">&quot;</span><span class="nv">$@</span><span class="s2">&quot;</span><span class="p">;</span> <span class="k">do</span>
    <span class="k">case</span> <span class="s2">&quot;</span><span class="nv">$arg</span><span class="s2">&quot;</span> in
        -a<span class="o">)</span> sleep <span class="m">1</span>.1 <span class="o">&amp;&amp;</span> <span class="nb">kill</span> -SIGTERM <span class="nv">$$</span> <span class="p">&amp;;;</span>
        -t<span class="o">)</span> <span class="nb">trap</span> <span class="s2">&quot;echo </span><span class="nv">$NAME</span><span class="s2">: exit on SIGTERM; exit&quot;</span> SIGTERM<span class="p">;;</span>
        -b<span class="o">)</span> <span class="nv">NAME</span><span class="o">=</span>sub_background /bin/bash <span class="nv">$0</span> -t <span class="p">&amp;;;</span>
        -f<span class="o">)</span> <span class="nv">FORK</span><span class="o">=</span><span class="m">1</span><span class="p">;;</span>
        -e<span class="o">)</span> <span class="nv">EXEC</span><span class="o">=</span><span class="m">1</span><span class="p">;;</span>
        *<span class="o">)</span> :<span class="p">;;</span>
    <span class="k">esac</span>
<span class="k">done</span>

<span class="nb">echo</span> <span class="s2">&quot;</span><span class="nv">$NAME</span><span class="s2">: running&quot;</span>

<span class="k">if</span> <span class="o">[</span> <span class="s2">&quot;</span><span class="nv">$FORK</span><span class="s2">&quot;</span> -a <span class="s2">&quot;</span><span class="nv">$EXEC</span><span class="s2">&quot;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
    <span class="nb">echo</span> <span class="s2">&quot;FORK and EXEC can not be set simultanously!&quot;</span>
    <span class="nb">exit</span> <span class="m">1</span>
<span class="k">elif</span> <span class="o">[</span> <span class="s2">&quot;</span><span class="nv">$EXEC</span><span class="s2">&quot;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
    <span class="nv">NAME</span><span class="o">=</span>sub_exec <span class="nb">exec</span> /bin/bash <span class="nv">$0</span> -t
<span class="k">elif</span> <span class="o">[</span> <span class="s2">&quot;</span><span class="nv">$FORK</span><span class="s2">&quot;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
    <span class="nv">NAME</span><span class="o">=</span>sub_fork /bin/bash <span class="nv">$0</span> -t
<span class="k">else</span>
    <span class="k">for</span> i in <span class="sb">`</span><span class="nb">eval</span> <span class="nb">echo</span> <span class="o">{</span><span class="m">1</span>..<span class="nv">$TICKS</span><span class="o">}</span><span class="sb">`</span><span class="p">;</span> <span class="k">do</span>
        sleep <span class="m">0</span>.1
        <span class="o">[</span> <span class="k">$((</span>i%10<span class="k">))</span> <span class="o">=</span> <span class="m">0</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&quot;</span><span class="nv">$NAME</span><span class="s2">: </span><span class="k">$((</span>i/10<span class="k">))</span><span class="s2">s TICK&quot;</span>
    <span class="k">done</span>
<span class="k">fi</span>

<span class="nb">echo</span> <span class="s2">&quot;</span><span class="nv">$NAME</span><span class="s2">: normal exit&quot;</span>
</pre></div>
<p>如果不加任何参数运行脚本，将显示以下输出，831是进程的PID：</p>
<pre class="literal-block">
main 831: running
main 831: 1s TICK
main 831: 2s TICK
main 831: normal exit
</pre>
<p>5个参数说明如下，其中<tt class="docutils literal"><span class="pre">-f</span></tt>和<tt class="docutils literal"><span class="pre">-e</span></tt>互斥：</p>
<ul class="simple">
<li><tt class="docutils literal"><span class="pre">-a</span></tt>：启动一个后台进程，其作用就是1.1秒后给主进程发送<tt class="docutils literal">SIGTERM</tt>信号</li>
<li><tt class="docutils literal"><span class="pre">-t</span></tt>：设置进程的SIGTERM处理函数</li>
<li><tt class="docutils literal"><span class="pre">-b</span></tt>：用同样的脚本启动一个后台进程，并设置其SIGTERM处理函数</li>
<li><tt class="docutils literal"><span class="pre">-f</span></tt>：不执行脚本的主体代码，而是用同样的脚本启动一个前台进程，并设置其SIGTERM处理函数</li>
<li><tt class="docutils literal"><span class="pre">-e</span></tt>：不执行脚本的主体代码，而是用<tt class="docutils literal">exec</tt>函数执行同样的脚本，并设置其SIGTERM处理函数</li>
</ul>
<p>脚本用环境变量<tt class="docutils literal">NAME</tt>区分不同的进程，主进程为<tt class="docutils literal">main</tt>，后台子进程和前台子进程分别是<tt class="docutils literal">sub_background</tt>和<tt class="docutils literal">sub_fork</tt>，用<tt class="docutils literal">exec</tt>执行的脚本为<tt class="docutils literal">sub_exec</tt>，虽然它和<tt class="docutils literal">main</tt>是同一个进程（替换了<tt class="docutils literal">main</tt>）。另一个环境变量<tt class="docutils literal">TIME_SECS</tt>用来控制脚本运行的时间，默认为2秒。</p>
<p>如果不启动子进程，则bash收到<tt class="docutils literal">SIGTERM</tt>便会执行处理函数，如果没有处理函数则直接退出：</p>
<pre class="literal-block">
$ bash test_signal.sh -a
main 665: running
main 665: 1s TICK
Terminated
$ bash test_signal.sh -a -t
main 680: running
main 680: 1s TICK
main 680: exit on SIGTERM
</pre>
<p>如果启动了前台子进程，则bash要等前台进程返回后才会执行<tt class="docutils literal">SIGTERM</tt>处理函数：</p>
<pre class="literal-block">
$ bash test_signal.sh -a -t -f
main 625: running
sub_fork 627: running
sub_fork 627: 1s TICK
sub_fork 627: 2s TICK
sub_fork 627: normal exit
main 625: exit on SIGTERM
</pre>
<p>后台子进程不影响bash执行<tt class="docutils literal">SIGTERM</tt>处理函数：</p>
<pre class="literal-block">
$ bash test_signal.sh -a -t -b
main 852: running
sub_background 854: running
main 852: 1s TICK
sub_background 854: 1s TICK
main 852: exit on SIGTERM
$ sub_background 854: 2s TICK
sub_background 854: normal exit
</pre>
<p>因为bash不会将收到的信号发送给子进程，在没有处理函数的情况下，它接收到处理函数后会马上退出并打印命令提示，但子进程不受影响，不管是前台的还是后台的子进程都会运行结束：</p>
<pre class="literal-block">
$ bash test_signal.sh -a -b -f
main 768: running
sub_background 770: running
sub_fork 771: running
sub_background 770: 1s TICK
sub_fork 771: 1s TICK
Terminated
$ sub_fork 771: 2s TICK
sub_fork 771: normal exit
sub_background 770: 2s TICK
sub_background 770: normal exit
</pre>
<p>如果用<tt class="docutils literal">exec</tt>执行脚本，将执行“子进程”的<tt class="docutils literal">SIGTERM</tt>处理函数，因为使用<tt class="docutils literal">exec</tt>后，原来的代码包括<tt class="docutils literal">SIGTERM</tt>处理函数都会被替换掉：</p>
<pre class="literal-block">
$ bash test_signal.sh -a -e
main 590: running
sub_exec 590: running
sub_exec 590: 1s TICK
sub_exec 590: exit on SIGTERM
</pre>
<p>再回到开头的问题，要让test_docker.sh脚本能处理<tt class="docutils literal">SIGTERM</tt>信号，就必须让脚本时常取得控制权，要达到这个目的，只要将监视进程放在后台，然后用<tt class="docutils literal">wait</tt>命令等待它：</p>
<div class="highlight"><pre><span></span><span class="ch">#!/bin/bash</span>

<span class="nb">trap</span> <span class="s1">&#39;stop_services; exit&#39;</span> SIGTERM SIGINT

<span class="nv">LOG_FILE</span><span class="o">=</span>/tmp/services.log
&gt;<span class="nv">$LOG_FILE</span>

service<span class="o">()</span> <span class="o">{</span>
    <span class="nb">local</span> <span class="nv">name</span><span class="o">=</span><span class="s2">&quot;</span><span class="nv">$1</span><span class="s2">&quot;</span>
    <span class="nb">local</span> <span class="nv">count</span><span class="o">=</span><span class="m">0</span>

    <span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
        <span class="nv">count</span><span class="o">=</span><span class="k">$((</span>count+1<span class="k">))</span>
        <span class="nb">echo</span> <span class="s2">&quot;</span><span class="nv">$name</span><span class="s2">: </span><span class="nv">$count</span><span class="s2">&quot;</span> &gt;&gt;<span class="nv">$LOG_FILE</span>
        sleep <span class="m">1</span>
    <span class="k">done</span>
<span class="o">}</span>

start_services<span class="o">()</span> <span class="o">{</span>
    <span class="nb">echo</span> <span class="s2">&quot;Start service 1&quot;</span>
    service s1 <span class="p">&amp;</span>
    <span class="nv">S1_PID</span><span class="o">=</span><span class="nv">$!</span>
    <span class="nb">echo</span> <span class="s2">&quot;Start service 2&quot;</span>
    service s2 <span class="p">&amp;</span>
    <span class="nv">S2_PID</span><span class="o">=</span><span class="nv">$!</span>
<span class="o">}</span>

stop_services<span class="o">()</span> <span class="o">{</span>
    <span class="nb">echo</span> <span class="s2">&quot;Stop service 2&quot;</span>
    <span class="nb">kill</span> <span class="nv">$S2_PID</span>
    <span class="nb">echo</span> <span class="s2">&quot;Stop service 1&quot;</span>
    <span class="nb">kill</span> <span class="nv">$S1_PID</span>
<span class="o">}</span>

<span class="nb">echo</span> <span class="s2">&quot;Script (PID=</span><span class="nv">$$</span><span class="s2">) running&quot;</span>

start_services

tail -f <span class="nv">$LOG_FILE</span> <span class="p">&amp;</span>
<span class="nb">wait</span>
</pre></div>
<p>测试结果：</p>
<pre class="literal-block">
$ docker run -d --name test -v /tmp:/tmp debian bash /tmp/test_docker.sh; docker stop test
e1cf8ddf25b32a1e533bc4beab7698071af8c40a334dcb1bd4c963e48d8c605b
test
$ docker logs test; docker inspect -f '{{.State.ExitCode}}' test; docker rm test
Script (PID=1) running
Start service 1
Start service 2
s2: 1
s1: 1
Stop service 2
Stop service 1
0
test
</pre>
<p>我一开始想的解决方法是将脚本的<tt class="docutils literal">SIGTERM</tt>处理函数替换为<tt class="docutils literal">EXIT</tt>处理函数，这在docker容器外能测试成功，bash收到<tt class="docutils literal">SIGTERM</tt>后会立即退出，并在退出前执行<tt class="docutils literal">EXIT</tt>处理函数，但是在docker容器内不行，因为这时bash的PID是1，只有当它设置了<tt class="docutils literal">SIGTERM</tt>处理函数后，才能接收到<tt class="docutils literal">SIGTERM</tt>信号。原因见<tt class="docutils literal">kill(2)</tt>文档：</p>
<blockquote>
The only signals that can be sent to process ID 1, the init process,
are  those  for  which  init  has  explicitly installed signal handlers.
This is done to assure the system is not brought down accidentally.</blockquote>
<p>编辑记录：</p>
<ul class="simple">
<li>2016-08-16，删除使用FIFO的解决方案，里面关于FIFO文件和read命令的解释是错误的</li>
<li>2016-08-21，用<tt class="docutils literal">wait</tt>命令代替死循环</li>
</ul>
<p>参考：</p>
<ul class="simple">
<li><a class="reference external" href="https://www.ctl.io/developers/blog/post/gracefully-stopping-docker-containers/">Gracefully Stopping Docker Containers</a></li>
</ul>
<p>（完）</p>

    </div>
<nav class="nav sidebar">
    <div class="siteinfo">
        <div class="sitename"><a href="../..">coinfaces</a></div>
        <div class="tagline">Another Side of Life</div>
    </div>

        <a href="../../categories/">categories</a>
        <a href="../..">&larr; Home</a>
</nav><footer class="footer">
    <span>
        &copy; jpli &middot;
        under <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">CC-BY-SA 4.0</a>
    </span>
    |
    <span>
        Powered by:
        <a href="http://blog.getpelican.com/">Pelican</a>
    </span>
</footer></body>
</html>